package org.pada.domain.cmd;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pada.client.dto.Cmd;
import org.pada.client.dto.Path;
import org.pada.client.dto.Resp;
import org.pada.client.exception.SystemErrorCode;
import org.pada.client.exception.SystemException;
import org.pada.client.util.JsonUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author shen.peng
 * @since 2024/10/25
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CmdHub {

    private final List<ICmdHandler> cmdHandlerList;

    private final RequestMappingHandlerMapping handlerMapping;

    private static final Map<Class, ICmdHandler> CMD_HANDLER_MAP =
            new ConcurrentHashMap<>();

    public static <R extends Resp, C extends Cmd<R>> R exec(C cmd) {
        ICmdHandler<C, R> iCmdHandler = CMD_HANDLER_MAP.get(cmd.getClass());
        if (Objects.isNull(iCmdHandler)) {
            throw new SystemException(SystemErrorCode.UN_DEFINE_ERR);
        }
        return iCmdHandler.exec(cmd);
    }


    @PostConstruct
    public void register() {
        cmdHandlerList.forEach(x -> {
            final Class<? extends Cmd<?>> cmdType = getCmdType(x.getClass());
            CMD_HANDLER_MAP.put(cmdType, x);
            String url = Optional.ofNullable(cmdType.getAnnotation(Path.class)).map(Path::path)
                    .orElse(cmdType.getName());
            RequestMappingInfo requestMapping = RequestMappingInfo.paths(url).methods(RequestMethod.POST).build();
            Route route = new Route() {
                @Override
                public @ResponseBody Object route(@RequestBody JsonNode body) {
                    log.info("provider url {} body {} ", url, body.toString());
                    Cmd<?> cmd = JsonUtil.convertObject(body, cmdType);
                    Resp exec = x.exec(cmd);
                    log.info("provider url {} response {} ", url, JsonUtil.toJSONString(exec));
                    return exec;
                }
            };
            handlerMapping.registerMapping(requestMapping, route, Route.class.getDeclaredMethods()[0]);
        });
    }

    private Class<? extends Cmd<?>> getCmdType(Class<?> cmdHandlerClz) {
        Method[] methods = cmdHandlerClz.getDeclaredMethods();
        for (Method method : methods) {
            if (isExecMethod(method)) {
                return checkAndGetCmdParamType(method);
            }
        }
        throw new RuntimeException("Cmd param in " + cmdHandlerClz + " " + "exec() is not detected");
    }

    private boolean isExecMethod(Method method) {
        return "exec".equals(method.getName()) && !method.isBridge();
    }

    private Class<? extends Cmd<?>> checkAndGetCmdParamType(Method method) {
        Class<?>[] exeParams = method.getParameterTypes();
        if (exeParams.length == 0) {
            throw new RuntimeException("Execute method in " + method.getDeclaringClass()
                    + " should at least have one parameter");
        }
        if (!Cmd.class.isAssignableFrom(exeParams[0])) {
            throw new RuntimeException(
                    "Execute method in " + method.getDeclaringClass() + " should be the subClass of Event");
        }
        return (Class<? extends Cmd<?>>) exeParams[0];
    }

    @FunctionalInterface
    public interface Route {
        @ResponseBody
        Object route(@RequestBody JsonNode body);
    }
}
